/*
 * Copyright Aristool AG Switzerland
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package org.opprua.tidy.beans;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.opprua.tidy.conversion.ChangerBroker;

/**
 * TODO: add Java doc
 *
 * Prepared for first commit: 15.07.2007
 *
 * @author Lin Li
 */
class BeanStringifier {

  private static int ARRAY_CUTOFF_SIZE = 20;
  private static int ARRAY_SAMPLE_SIZE = 4;
  private static int STRING_CUTOFF_SIZE = 250;
  private static int STRING_SAMPLE_SIZE = 20;
  private static String INDENT_STRING = ".    ";

  private static class MapElement {

    public Object key;
    public Object value;

    public static MapElement[] getEntries(Map<Object, Object> map) {
      MapElement[] m = new MapElement[map.size()];
      Set<Object> entries = map.keySet();
      int i = 0;
      for (Iterator<Object> iter = entries.iterator(); iter.hasNext(); i++) {
        Object e = iter.next();
        m[i] = new MapElement();
        m[i].key = e;
        m[i].value = map.get(e);
      }
      return m;
    }
  }

  private BeanStringifier() {
  }

  /**
   */
  public static String stringify(Object object) {
    StringBuffer buf = new StringBuffer();
    write(object, buf, "", false);
    return buf.toString();
  }
  
  @SuppressWarnings("unchecked")
  private static void write(Object object, StringBuffer buf, String indent, boolean forWeb) {

    // do not throw up if it is a null case
    if (object == null) {
      buf.append("null").append("\n");
      return;
    }

    Class<?> oclass = object.getClass();

    if (object instanceof Throwable) {
      Throwable t = (Throwable) object;
      buf.append(t.getMessage()).append("\n");
    } else if (oclass == String.class || oclass == StringBuffer.class) {
      String s = object.toString();
      if (s.length() > STRING_CUTOFF_SIZE && s.length() > STRING_SAMPLE_SIZE) {
        s = s.substring(0, STRING_SAMPLE_SIZE) + " ... <total length=" + s.length() + ">";
      }
      buf.append("\"").append(s).append("\"").append("\n");
    } else if (ChangerBroker.getDefaultValue(oclass) != null) {
      buf.append(ChangerBroker.convert(String.class, object)).append("\n");
    } else if (oclass.isArray()) {
      writeArray(oclass, object, buf, indent, forWeb);
    } else if (object instanceof Map) {
      writeArray(oclass, MapElement.getEntries((Map<Object, Object>) object), buf, indent, forWeb);
    } else if (object instanceof Collection) {
      writeArray(oclass, ((Collection<Object>) object).toArray(), buf, indent, forWeb);
    } else if (BeanField.getFields(oclass).size() == 0) {
      Method m = null;
      try {
        m = oclass.getMethod("value", new Class<?>[0]);
        if (m.getReturnType() != int.class) {
          m = null;
        }
      } catch (Exception e) {
      }
      String value = null;
      if (m != null) {
        try {
          Integer v = null;
          v = (Integer) m.invoke(object, new Object[0]);
          if (v != null) {
            Field[] fs = oclass.getFields();
            for (int i = 0; i < fs.length; i++) {
              Field field = fs[i];
              if (Modifier.isStatic(field.getModifiers())) {
                if (v.equals(field.get(object))) {
                  value = oclass.getName() + ": " + field.getName();
                  break;
                }
              }
            }
          }
        } catch (Exception e) {
        }
      }
      if (value == null) {
        value = object.toString();
      }
      buf.append(value).append("\n");
    } else {
      writeBlock(oclass, object, buf, indent, forWeb);
    }
  }

  private static void writeBlock(Class<?> oclass, Object object, StringBuffer buf, String indent,
              boolean forWeb) {

    Method m = null;
    String value = null;
    try {
      m = oclass.getMethod("toDescriptor", new Class<?>[0]);
      if (m.getReturnType() == String.class) {
        value = (String) m.invoke(object, new Object[0]);
      } else if (m.getReturnType() == int.class) {
        value = ((Integer) m.invoke(object, new Object[0])).toString();
      }
    } catch (Exception e) {
    }
    if (value != null) {
      buf.append(value).append("\n");
      return;
    }

    BeanField[] fields = (BeanField[]) BeanField.getFields(oclass).values().toArray(new BeanField[0]);

    buf.append(oclass.getName()).append(": \n");
    indent += INDENT_STRING;

    /** write out "name = value" */
    for (int i = 0; i < fields.length; i++) {
      buf.append(indent).append(fields[i].getName()).append(" = ");
      /** write out substructure */
      write(fields[i].get(object), buf, indent, forWeb);
    }
  }

  private static void writeArray(Class<?> oclass, Object object, StringBuffer buf, String indent,
              boolean forWeb) {

    int size = Array.getLength(object);
    int upper = size;
    if (upper > ARRAY_CUTOFF_SIZE && upper > ARRAY_SAMPLE_SIZE) {
      upper = ARRAY_SAMPLE_SIZE;
    }

    Class<?> t = oclass.getComponentType();
    if (t != null && t.isPrimitive()) {
      buf.append("{ ");
      indent += INDENT_STRING;
      for (int i = 0; i < upper; i++) {
        if (i != 0) {
          buf.append(", ");
        }
        if (i % 10 == 0) {
          buf.append("\n").append(indent);
        }
        buf.append(Array.get(object, i));
      }
      if (upper < size) {
        buf.append(", ... <total = ").append(size).append(">");
      }
      buf.append("}\n");
    } else if (t != null && t.equals(String.class)) {
      buf.append("{ ");
      indent += INDENT_STRING;
      for (int i = 0; i < upper; ++i) {
        buf.append("\n").append(indent);
        String shere = (String) Array.get(object, i);
        if (shere != null) {
          if (shere.length() > STRING_CUTOFF_SIZE) {
            shere = shere.substring(0, STRING_SAMPLE_SIZE) + " ... <total length="
                        + shere.length() + ">";
          }
          buf.append('"').append(shere).append('"');
        } else {
          buf.append("null");
        }
      }
      if (upper < size) {
        buf.append(", ... <total number of strings = ").append(size).append(">");
      }
      buf.append("}\n");
    } else {
      if (t != null) {
        buf.append(t).append('[').append(size).append("]").append("\n");
      } else {
        buf.append(oclass).append("\n");
      }
      for (int i = 0; i < upper; ++i) {
        String id = "" + i;
        int isize = INDENT_STRING.length();
        if (id.length() + 2 > isize) {
          id = id + " ";
        } else {
          id = id + " " + INDENT_STRING.substring(isize - id.length() - 2);
        }
        String nindent = indent + id;
        buf.append(nindent);
        write(Array.get(object, i), buf, nindent, forWeb);
      }
      if (upper < size) {
        buf.append(indent).append("==== <total number of elements = ").append(size).append(">\n");
      }
    }
  }
}
